package com.glodon.paas.document.storage;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.commons.lang.StringUtils;

import com.glodon.paas.document.storage.dao.StorageDAO;
import com.glodon.paas.document.storage.directory.Directory;
import com.glodon.paas.document.storage.id.ObjectId;
import com.glodon.paas.document.storage.id.ObjectResult;
import com.glodon.paas.document.storage.model.Node;
import com.glodon.paas.document.storage.model.NodeAssoc;
import com.glodon.paas.document.storage.model.Store;
import com.glodon.paas.document.storage.util.DigestState;
import com.glodon.paas.document.storage.util.VerifyInputStream;

public class Storage {

    private static final char SEPARATOR = '/';
    private Directory directory;
    private String name;
    private StorageDAO storageDAO;

    public Storage() {
    }

    public void save(String path, InputStream is) throws IOException {
        String fileName = getFileName(path);
        Node folder = makeParentDir(path);
        ObjectResult objectResult = directory.save(is);
        Node file = storageDAO.getChildNode(folder.getId(), fileName);
        if (file == null) {
            file = new Node();
            file.setType(NodeType.file);
            file.setLength(objectResult.getLength());
            storageDAO.addChildNode(folder, file, fileName);
        }
        addRevision(file, objectResult.getObjectId(), objectResult.getLength());
    }

    public String initPart(String path) throws IOException {
        String fileName = getFileName(path);
        Node folder = makeParentDir(path);
        Node file = storageDAO.getChildNode(folder.getId(), fileName);
        Node part = null;
        if (file == null) {
            file = new Node();
            file.setType(NodeType.file);
            storageDAO.addChildNode(folder, file, fileName);
        } else {
            List<Node> revisions = getRevisions(file);
            Node lastRevision = revisions.get(revisions.size() - 1);
            if (lastRevision.getType() == NodeType.part) {
                part = lastRevision;
                String partId = part.getPartId();
                directory.abortPart(partId);
            }
        }
        if (part == null) {
            part = new Node();
            part.setType(NodeType.part);
            addRevisionOrPart(file, part);
        }
        String partId = directory.initPart();
        part.setPartId(partId);
        storageDAO.updateNode(part);
        return partId;
    }

    public String append(String path, String partId, int partNumber, InputStream is, long length) throws IOException {
        Node part = null;
        Node file = getNodeByPath(path);
        List<Node> nodes = getRevisions(file);
        for (Node node : nodes) {
            if (node.getType() == NodeType.part) {
                part = node;
            }
        }
        if (part == null)
            throw new IOException("part not found: " + partId);
        try {
            ObjectResult objectResult = directory.append(partId, partNumber, part.getState(), is, length);
            part.setObjectId(objectResult.getObjectId());
            DigestState digestState = objectResult.getDigestState();
            part.setState(digestState.toString());
            part.setLength(part.getLength() + objectResult.getLength());
            storageDAO.updateNode(part);
            return objectResult.getObjectId();
        } catch (IOException e) {
            storageDAO.deleteNode(file, part, getFileName(path));
            throw e;
        }
    }

    private Node getPartByPath(String path) {
        Node file = getNodeByPath(path);
        List<Node> nodes = getRevisions(file);
        for (Node node : nodes) {
            if (node.getType() == NodeType.part) {
                return node;
            }
        }
        return null;
    }

    public String completePart(String path, String partId) throws IOException {
        Node part = getPartByPath(path);
        if (part == null)
            throw new IOException("part not found: " + partId);
        String objectId = ObjectId.fromRaw(DigestState.getDigestState(part.getState()).getMessageDigest().digest())
                .name();
        directory.complete(partId, objectId);
        part.setObjectId(objectId);
        long length = part.getLength();
        part.setLength(length);
        part.setType(NodeType.revision);
        storageDAO.updateNode(part);
        Node file = getNodeByPath(path);
        file.setObjectId(objectId);
        file.setLength(length);
        storageDAO.updateNode(file);
        return objectId;
    }

    private String getFileName(String path) {
        return path.substring(path.lastIndexOf(SEPARATOR) + 1);
    }

    private Node makeParentDir(String path) {
        return mkdir(getParentPath(path));
    }

    private String getParentPath(String path) {
        return path.substring(0, path.lastIndexOf(SEPARATOR));
    }

    private Node mkdir(String path) {
        String[] paths = getPaths(path);
        Node parent = getRootNode();
        for (String name : paths) {
            NodeAssoc nodeAssoc = storageDAO.getNodeAssoc(parent.getId(), name);
            if (nodeAssoc == null) {
                Node node = new Node();
                node.setType(NodeType.folder);
                storageDAO.addChildNode(parent, node, name);
                parent = node;
            } else {
                parent = nodeAssoc.getChild();
                if (parent.isDeleted())
                    restore(parent);
            }
        }
        return parent;
    }

    private String[] getPaths(String path) {
        return StringUtils.split(path, SEPARATOR);
    }

    private void restore(Node node) {
        if (node.isDeleted()) {
            if (node.isFile()) {
                addRevision(node, node.getObjectId(), node.getLength());
            } else {
                node.setDeleted(false);
                storageDAO.updateNode(node);
            }
        }
    }

    private Store getStore(String name) {
        return storageDAO.getStoreByName(name);
    }

    public InputStream get(String path) throws IOException {
        return getFile(path).getInputStream();
    }

    public InputStream get(String path, long start) throws IOException {
        StorageFile storageFile = getFile(path);
        return storageFile.getInputStream(start);
    }

    private Node getNodeByPath(String path) {
        String[] paths = getPaths(path);
        Node parent = getRootNode();
        for (String name : paths) {
            NodeAssoc nodeAssoc = storageDAO.getNodeAssoc(parent.getId(), name);
            if (nodeAssoc == null) {
                return null;
            } else {
                parent = nodeAssoc.getChild();
            }
        }
        return parent;
    }

    private Node getRootNode() {
        Store store = getStore(name);
        return store.getRoot();
    }

    public void setDirectory(Directory directory) {
        this.directory = directory;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setStorageDAO(StorageDAO storageDAO) {
        this.storageDAO = storageDAO;
    }

    public StorageFile[] getRevisions(String path) {
        Node file = getNodeByPath(path);
        if (file == null)
            return null;
        List<Node> revisions = getRevisions(file);
        StorageFile[] storageFiles = new StorageFile[revisions.size()];
        for (int i = 0; i < revisions.size(); i++) {
            Node revision = revisions.get(i);
            storageFiles[i] = new StorageFile(this, revision);
        }
        return storageFiles;
    }

    private List<Node> getRevisions(Node file) {
        List<Node> revisions = storageDAO.getChildNodes(file.getId());
        return revisions;
    }

    InputStream getInputStreamById(String objectId, long start) throws IOException {
        if (start == 0) {
            return new VerifyInputStream(objectId, directory.get(objectId));
        } else {
            return directory.get(objectId, start);
        }
    }

    public StorageFile getFile(String path) {
        Node node = getNodeByPath(path);
        if (node == null)
            return null;
        return new StorageFile(this, node);
    }

    public boolean del(String path) {
        Node node = getNodeByPath(path);
        if (node == null)
            return false;
        del(node);
        return true;
    }

    private Node addRevision(Node file, String objectId, long length) {
        return addRevisionOrPart(file, null, objectId, NodeType.revision, length);
    }

    private Node addRevisionOrPart(Node file, String id, String objectId, NodeType nodeType, long length) {
        Node node = new Node();
        node.setId(id);
        node.setObjectId(objectId);
        node.setType(nodeType);
        node.setLength(length);
        addRevisionOrPart(file, node);
        return node;
    }

    private void addRevisionOrPart(Node file, Node newRevisionOrPart) {
        storageDAO.addChildNode(file, newRevisionOrPart, null);
        file.setObjectId(newRevisionOrPart.getObjectId());
        file.setDeleted(false);
        storageDAO.updateNode(file);
    }

    private void addDeletedRevision(Node file) {
        Node node = new Node();
        node.setObjectId(file.getObjectId());
        node.setType(NodeType.revision);
        node.setLength(file.getLength());
        node.setDeleted(true);
        storageDAO.addChildNode(file, node, null);
    }

    private void del(Node node) {
        if (node.isFolder()) {
            List<Node> childNodes = getRevisions(node);
            for (Node childNode : childNodes) {
                del(childNode);
            }
        }
        node.setDeleted(true);
        storageDAO.updateNode(node);
        if (node.isFile())
            addDeletedRevision(node);
    }

    public void link(String to, String from) {
        Node dir = makeParentDir(from);
        Node toNode = getNodeByPath(to);
        storageDAO.addChildAssoc(dir.getId(), toNode.getId(), getFileName(from));
    }

    public void purge(String path) {
        Node parent = getNodeByPath(getParentPath(path));
        Node node = getNodeByPath(path);
        storageDAO.deleteNode(parent, node, getFileName(path));
    }

    public void mkDir(String path) {
        mkdir(path);
    }

    public void restore(String path) {
        Node node;
        String[] paths = getPaths(path);
        Node parent = getRootNode();
        for (String name : paths) {
            NodeAssoc nodeAssoc = storageDAO.getNodeAssoc(parent.getId(), name);
            if (nodeAssoc == null) {
                node = null;
            } else {
                parent = nodeAssoc.getChild();
                restore(parent);
            }
        }
        node = parent;
        if (node != null) {
            restoreAllChildNode(node);
        }
    }

    private void restoreAllChildNode(Node node) {
        restore(node);
        if (node.isFolder()) {
            List<Node> childNodes = getRevisions(node);
            for (Node childNode : childNodes) {
                restoreAllChildNode(childNode);
            }
        }
    }

    public String getName() {
        return name;
    }
}
